package service

import (
	"context"
	"database/sql"
	"errors"
	"math"
	"sort"

	"gitee.com/hp-huiw/family-expense-api/internal/constant"
	dbmodel "gitee.com/hp-huiw/family-expense-api/internal/db/model"
	"gitee.com/hp-huiw/family-expense-api/internal/dto"
	"gitee.com/hp-huiw/family-expense-api/internal/repository"
	"gitee.com/hp-huiw/my-go-core/date"
	"gitee.com/hp-huiw/my-go-core/util"
	"github.com/samber/lo"
	"github.com/samber/lo/mutable"
)

type JourneyIncomeExpenseDetailService interface {
	QueryExistingDistinctYears(ctx context.Context, accountGroupID string) ([]int, error)
	Query(ctx context.Context, journeyID string, queryArgument dto.IncomeExpenseDetailQueryArgumentRequest) ([]dto.JourneyIncomeExpenseDetailResponse, error)
	QueryDetails(ctx context.Context, accountGroupID string, id string) (*dto.JourneyIncomeExpenseFullAddressDetailResponse, error)
	QueryCoordinates(ctx context.Context, accountGroupID string, year string) ([]dto.FormattedAddressCoordinate, error)
	Exists(ctx context.Context, accountGroupID string, journeyID string) (bool, error)
	Add(ctx context.Context, data *dto.JourneyIncomeExpenseDetailResquest, accountGroupID string, username string) error
	Update(ctx context.Context, accountGroupID string, username string, data *dto.JourneyIncomeExpenseDetailResquest) error
	Delete(ctx context.Context, accountGroupID string, ids []string) error
	DeleteWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error
}

func NewJourneyIncomeExpenseDetailService(db *sql.DB) JourneyIncomeExpenseDetailService {
	return &journeyIncomeExpenseDetailService{
		db:                                   db,
		journeyIncomeExpenseDetailRepository: repository.NewJourneyIncomeExpenseDetailRepository(db),
		bankService:                          NewBankService(db),
		bankCardService:                      NewBankCardService(db),
		userService:                          NewUserService(db),
	}
}

// QueryExistingDistinctYears queries all distinct income/expense years.
// return The list of Integer of existing year.
func (s *journeyIncomeExpenseDetailService) QueryExistingDistinctYears(ctx context.Context, accountGroupID string) ([]int, error) {
	years, err := s.journeyIncomeExpenseDetailRepository.QueryExistingDistinctYears(ctx, accountGroupID)

	if err != nil {
		return nil, err
	}

	sort.Ints(years)
	mutable.Reverse(years)

	return years, nil
}

// Query queries all journey income/expense which associated with provided account group.
func (s *journeyIncomeExpenseDetailService) Query(ctx context.Context, journeyID string, queryArgument dto.IncomeExpenseDetailQueryArgumentRequest) ([]dto.JourneyIncomeExpenseDetailResponse, error) {
	query, err := s.journeyIncomeExpenseDetailRepository.Query(ctx, journeyID, queryArgument.AccountGroupID, queryArgument.Keyword, queryArgument.PageSize, queryArgument.PageIndex)

	if err != nil {
		return nil, err
	}

	result := lo.Map(query, func(item repository.JourneyIncomeExpenseDetail, _ int) dto.JourneyIncomeExpenseDetailResponse {
		return dto.JourneyIncomeExpenseDetailResponse{
			JourneyIncomeExpenseDetail: dto.JourneyIncomeExpenseDetail{
				ID:          item.ID,
				Location:    item.Location,
				Currency:    item.Currency,
				Amount:      item.Amount,
				Type:        item.Type,
				PaymentType: item.PaymentType,
				Details:     item.Details,
			},
			BankCardInfo: dto.BankCardInfo{
				BankID:               item.BankID,
				BankShortName:        item.BankShortName,
				BankLogoUrl:          s.bankService.GetLogoUrl(item.BankShortName),
				BankCardName:         item.BankCardName,
				BankCardIsCreditCard: item.BankCardIsCreditCard,
			},
			IncomeExpenseDate: item.IncomeExpenseDate,
			JourneyID:         item.JourneyID,
			JourneyStartDate:  item.JourneyStartDate,
			JourneyEndDate:    item.JourneyEndDate,
			RegionTitle:       item.RegionTitle,
		}
	})

	return result, nil
}

// QueryDetails queries journey income expense details by its id.
func (s *journeyIncomeExpenseDetailService) QueryDetails(ctx context.Context, accountGroupID string, id string) (*dto.JourneyIncomeExpenseFullAddressDetailResponse, error) {
	query, err := s.journeyIncomeExpenseDetailRepository.QueryDetails(ctx, accountGroupID, id)

	if err != nil {
		return nil, err
	}

	if query == nil {
		return nil, nil
	}

	// Queries createdBy & updatedBy basic info.
	userDetails, err := s.userService.QueryBasicInfo(ctx, accountGroupID, []string{*query.CreatedByStr, *query.UpdatedByStr})

	if err != nil {
		return nil, err
	}

	createdBy := lo.Filter(userDetails, func(item dto.UserBasicInfoResponse, _ int) bool {
		return item.Username == *query.CreatedByStr
	})[0]

	updatedBy := lo.Filter(userDetails, func(item dto.UserBasicInfoResponse, _ int) bool {
		return item.Username == *query.UpdatedByStr
	})[0]

	result := dto.JourneyIncomeExpenseFullAddressDetailResponse{
		JourneyIncomeExpenseDetailResponse: dto.JourneyIncomeExpenseDetailResponse{
			JourneyIncomeExpenseDetail: dto.JourneyIncomeExpenseDetail{
				ID:          query.ID,
				Location:    query.Location,
				Currency:    query.Currency,
				Amount:      query.Amount,
				Type:        query.Type,
				PaymentType: query.PaymentType,
				Details:     query.Details,
			},
			BankCardInfo: dto.BankCardInfo{
				BankID:               query.BankID,
				BankShortName:        query.BankShortName,
				BankLogoUrl:          s.bankService.GetLogoUrl(query.BankShortName),
				BankCardName:         query.BankCardName,
				BankCardIsCreditCard: query.BankCardIsCreditCard,
			},
			IncomeExpenseDate: query.IncomeExpenseDate,
			JourneyID:         query.JourneyID,
			JourneyStartDate:  query.JourneyStartDate,
			JourneyEndDate:    query.JourneyEndDate,
			RegionTitle:       query.RegionTitle,
		},
		FullAddressCoordinate: dto.FullAddressCoordinate{
			FormattedAddressCoordinate: dto.FormattedAddressCoordinate{
				Latitude:         query.Latitude,
				Longitude:        query.Longitude,
				FormattedAddress: query.FormattedAddress,
			},
			Province: query.Province,
			City:     query.City,
			District: query.District,
		},
		AuditInfo: dto.AuditInfo{
			CreatedBy: dto.UserAudit{
				ID:        &createdBy.ID,
				Username:  &createdBy.FullName,
				FullName:  &createdBy.FullName,
				AvatarUrl: &createdBy.AvatarUrl,
			},
			UpdatedBy: dto.UserAudit{
				ID:        &updatedBy.ID,
				Username:  &updatedBy.FullName,
				FullName:  &updatedBy.FullName,
				AvatarUrl: &updatedBy.AvatarUrl,
			},
			Timestamps: dto.Timestamps{
				DateCreated: query.DateCreated,
				LastUpdated: query.LastUpdated,
			},
		},
	}

	return &result, nil
}

// QueryCoordinates queries coordinates by account group id and year.
func (s *journeyIncomeExpenseDetailService) QueryCoordinates(ctx context.Context, accountGroupID string, year string) ([]dto.FormattedAddressCoordinate, error) {
	query, err := s.journeyIncomeExpenseDetailRepository.QueryCoordinates(ctx, accountGroupID, year)

	if err != nil {
		return nil, err
	}

	result := lo.Map(query, func(item repository.FormattedAddressCoordinate, _ int) dto.FormattedAddressCoordinate {
		return dto.FormattedAddressCoordinate{
			Latitude:         item.Latitude,
			Longitude:        item.Longitude,
			FormattedAddress: item.FormattedAddress,
		}
	})

	return result, nil
}

// Exists checks if the JourneyIncomeExpenseDetail exists by accountGroup id and journey id.
func (s *journeyIncomeExpenseDetailService) Exists(ctx context.Context, accountGroupID string, journeyID string) (bool, error) {
	return s.journeyIncomeExpenseDetailRepository.Exists(ctx, accountGroupID, journeyID)
}

// Add adds a new record of JourneyIncomeExpenseDetail with posted data.
// The username used to query the account group id and saved to createdBy and updatedBy.
func (s *journeyIncomeExpenseDetailService) Add(ctx context.Context, data *dto.JourneyIncomeExpenseDetailResquest, accountGroupID string, username string) error {
	model, err := s.mapToDBModel(*data, username)

	if err != nil {
		return err
	}

	model.ID = util.GenerateUUIDWithoutDashes()
	model.AccountGroupID = accountGroupID
	model.CreatedBy = username
	model.DateCreated = date.GetCurrentDate()
	model.JourneyID = data.Journey.ID

	return s.journeyIncomeExpenseDetailRepository.Insert(ctx, model)
}

// Updates existing JourneyIncomeExpenseDetail with posted data.
func (s *journeyIncomeExpenseDetailService) Update(ctx context.Context, accountGroupID string, username string, data *dto.JourneyIncomeExpenseDetailResquest) error {
	model, err := s.mapToDBModel(*data, username)

	if err != nil {
		return err
	}

	model.ID = data.ID

	return s.journeyIncomeExpenseDetailRepository.Update(ctx, accountGroupID, model)
}

// Deletes the IncomeExpenseDetails with provides list of id.
func (s *journeyIncomeExpenseDetailService) Delete(ctx context.Context, accountGroupID string, ids []string) error {
	if len(ids) > 0 {
		return s.journeyIncomeExpenseDetailRepository.Delete(ctx, accountGroupID, ids)
	}

	return errors.New("ids is empty")
}

// DeleteWithTx deletes all income and expense details by account group id with transaction.
func (s *journeyIncomeExpenseDetailService) DeleteWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error {
	return s.journeyIncomeExpenseDetailRepository.DeleteWithTx(ctx, tx, accountGroupID)
}

func (*journeyIncomeExpenseDetailService) mapToDBModel(data dto.JourneyIncomeExpenseDetailResquest, username string) (*dbmodel.JourneyIncomeExpenseDetail, error) {
	incomeExpenseDate, err := date.ParseFromLongDateTime(data.IncomeExpenseDate)

	if err != nil {
		return nil, err
	}

	// Just in case user inputs negative number for income and positive number for expense.
	// We take the absolute value first.
	amount := math.Abs(data.Amount)

	// If it is expense we take it as negative number.
	if data.Type == constant.IncomeExpenseTypeExpense {
		amount = -amount
	}

	model := dbmodel.JourneyIncomeExpenseDetail{
		IncomeExpenseDate: incomeExpenseDate,
		Location:          data.Location,
		Currency:          util.AnyPtr(data.Currency),
		Amount:            amount,
		Type:              data.Type,
		PaymentType:       util.AnyPtr(data.PaymentType),
		Details:           data.Details,
		UpdatedBy:         username,
		LastUpdated:       date.GetCurrentDate(),
	}

	if data.BankCard != nil {
		model.BankCardID = &data.BankCard.ID
	}

	return &model, nil
}

// Container of IncomeExpenseDetailService functions.
type journeyIncomeExpenseDetailService struct {
	db                                   *sql.DB
	journeyIncomeExpenseDetailRepository repository.JourneyIncomeExpenseDetailRepository
	bankService                          BankService
	bankCardService                      BankCardService
	userService                          UserService
}
